#!/usr/bin/env python
# -*- coding:utf-8 -*-
import codecs
import os
import uuid
from abc import abstractmethod

from utils import info, env, Fail, run_cmd, prepare_dir, Assert, cpfile, wildone, AssertMac, replace_in_file, \
    AssertNotNull, grep_file, grep, Never, head, md5, try_run, ZIP


class Mode:
    DEBUG = "debug"
    RELEASE = "release"
    ENTERPRISE = "release"
    STORE = "store"
    ALL = [DEBUG, RELEASE, ENTERPRISE, STORE]

    def __init__(self):
        pass

    @staticmethod
    def parse(mode):
        mode = str(mode).lower().strip()
        if mode == "all":
            return Mode.ALL
        elif mode == "ios":
            return Mode.DEBUG
        elif mode == "release":
            return Mode.RELEASE
        elif mode == "store":
            return Mode.STORE
        elif mode == "enterprise":
            return Mode.ENTERPRISE


class Platform:
    iOS = "ios"
    Android = "android"
    ALL = "all"

    def __init__(self):
        pass

    @staticmethod
    def parse(platform):
        platform = str(platform).lower().strip()
        if platform == "all":
            return Platform.ALL
        elif platform == "ios":
            return Platform.iOS
        elif platform == "android":
            return Platform.Android


class BuilderInfo:
    def __init__(self):
        self.title = "UNKNOWN_MOBILE_APP"
        self.version = '1'
        self.basedir = './'
        self.output = 'bin'
        self.bundleID = None
        self.vars = {}

    def exists(self, key, pwd=None):
        if pwd is None:
            pwd = self.basedir
        path = self.get(key)
        if not path.startswith('/'):
            path = os.path.join(pwd, path)
            path = os.path.abspath(path)
        Assert("配置[%s]的文件[%s]不存在" % (key, path), os.path.exists(path))

    def has(self, key):
        if key in self.vars:
            return True
        Assert("没有设置[%s]" % key, key in self.__dict__)

    def get(self, key, allowNone=False):
        if key in self.vars:
            ret = self.vars[key]
        else:
            Assert("没有设置[%s]" % key, key in self.__dict__, fail_level=1)
            ret = self.__dict__[key]
        if ret is None and not allowNone:
            Fail("设置[%s]为None" % key, level=1)
        return ret

    def set(self, key, value):
        self.vars[key] = value


class Base:
    def __init__(self, builder, verbose=True):
        self.title = builder.title
        self.version = builder.version
        self.builder = builder
        self.builder.output = os.path.abspath(self.builder.output)
        self.output = builder.output
        self._pwd = None
        self.basedir = builder.basedir
        self.target = [Mode.DEBUG, Mode.RELEASE]
        self.verbose = verbose
        self.tmpdir = prepare_dir('tmp', pwd=builder.basedir)

    @abstractmethod
    def prepare(self):
        """
        准备
        """
        pass

    @abstractmethod
    def build(self):
        """
        构建
        """
        pass

    def cur(self, path):
        return os.path.join(self._pwd, path)

    def cd(self, pwd=None):
        if pwd is None:
            self._pwd = self.basedir
        else:
            self._pwd = pwd

    def run(self, cmd, fail=True, hide=None):
        buff = []
        run_cmd(cmd, fail=fail, pwd=self._pwd, hide=hide, verbose=self.verbose, output=True, stdout_buff=buff)
        return buff

    def arch(self, from_path, to_filename, remove=False):
        from_path = os.path.abspath(from_path)
        to_filename = os.path.join(self.builder.output, to_filename)
        to_filename = os.path.abspath(to_filename)
        from_path = wildone(from_path, pwd=self._pwd)
        Assert("文件[%s]不存在" % from_path, os.path.exists(from_path))
        if os.path.isdir(from_path):
            info("ARCH_DIR[%s]" % to_filename)
            ZIP(to_filename, dirs_or_files=from_path, verbose=True)
        else:
            info("ARCH[%s]" % to_filename)
            cpfile(from_path, to_filename, remove=remove)

    def temp(self, ext='tmp', create_empty=True):
        ext = ext.strip().lstrip('.')
        name = md5(uuid.uuid1().bytes)

        def path(filename):
            return os.path.join(self.tmpdir, '%s.%s' % (filename, ext))

        if os.path.exists(path(name)):
            name = md5(uuid.uuid1())
        if os.path.exists(path(name)):
            Fail("创建临时文件失败")
        if create_empty:
            fout = open(path(name), 'wb')
            fout.close()
        return path(name)


class iOSBuilder(Base):
    def __init__(self, builder, verbose=True):
        Base.__init__(self, builder=builder, verbose=verbose)
        self.provisioning = None
        self.keychains = None
        self.pbxproj = None
        self.infoPlist = None
        self.keychains_passwd = None
        self.codeSign = None
        self.basedir = builder.get('proj.ios')
        self.app = None
        self.plist = None
        self.orig_keychains = None
        self.orig_default_keychain = None
        self.dSYM = None

    def prepare(self):
        AssertMac()
        target = self.target
        self.builder.bundleID = self.builder.get('%s.bundleID' % target)
        self.codeSign = self.builder.get('%s.codeSign' % target)
        self.provisioning = self.builder.get('%s.provisioning' % target)
        self.keychains = self.builder.get('%s.keychains' % target)
        self.keychains_passwd = self.builder.get('%s.keychains.passwd' % target)
        self.pbxproj = self.builder.get('project.pbxproj')
        self.infoPlist = self.builder.get('Info.plist')
        self.plist = self.builder.get('%s.plist' % target, allowNone=True)
        self.orig_keychains = self.run('/usr/bin/security list-keychains')
        self.orig_keychains = map(lambda x: x.strip(), self.orig_keychains)
        self.orig_keychains.remove('"/Library/Keychains/System.keychain"')
        self.orig_default_keychain = self.run('/usr/bin/security default-keychain', fail=False)
        if len(self.orig_default_keychain) == 0:
            self.orig_default_keychain = ""
        else:
            self.orig_default_keychain = self.orig_default_keychain[0].strip()
        info("default keychain[%s]" % self.orig_default_keychain)
        info("current keychains[%s]" % ' '.join(self.orig_keychains))
        # todo:装在最新的证书(应对ITMS-90034)
        # https://developer.apple.com/certificationauthority/AppleWWDRCA.cer

    def update_version(self):
        self.run('/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString %s%s" %s' % (
            self.builder.get('%s.human_version' % self.target), self.builder.version, self.infoPlist
        ))
        self.run('/usr/libexec/PlistBuddy -c "Set :CFBundleVersion %s%s" %s' % (
            self.builder.get('%s.build_version' % self.target), self.builder.version, self.infoPlist
        ))

    def updateID(self):
        self.run('/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier %s" %s' % (
            self.builder.bundleID, self.builder.get("Info.plist")))
        replace_in_file(self.builder.get('project.pbxproj'),
                        'PRODUCT_BUNDLE_IDENTIFIER = .+;',
                        'PRODUCT_BUNDLE_IDENTIFIER = %s;' % self.builder.bundleID,
                        found=False
                        )
        # replace_in_file(self.builder.get('project.pbxproj'),
        #                 'CODE_SIGN_IDENTITY[sdk=iphoneos*]" = .+;',
        #                 'PRODUCT_BUNDLE_IDENTIFIER = %s;' % self.codeSign,
        #                 found=False
        #                 )
        provisioning = grep_file(self.provisioning,
                                 '<string>([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})</string>',
                                 group=1, num=1)
        replace_in_file(self.pbxproj,
                        'PROVISIONING_PROFILE = .+;',
                        'PROVISIONING_PROFILE = "%s";' % provisioning,
                        found=False
                        )
        replace_in_file(self.pbxproj,
                        '"PROVISIONING_PROFILE\[sdk=.+\]".+;',
                        '',
                        found=False
                        )

    def update_keychains(self):
        self.run('/usr/bin/security list-keychains -s #%s#' % self.keychains, hide=self.keychains)
        self.run('/usr/bin/security default-keychain -d user -s #%s#' % self.keychains, hide=self.keychains)
        self.run('/usr/bin/security unlock-keychain -p #%s# #%s#' % (self.keychains_passwd, self.keychains),
                 hide=[self.keychains_passwd, self.keychains])
        # self.run('/usr/bin/security show-keychain-info #%s#' % keychains, hide=keychains)
        self.run('/usr/bin/security find-identity -p codesigning -v')

    def package(self):
        AssertNotNull("找不到生成的app", self.app)
        plist = self.builder.get('Info.plist')
        self.run('/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" %s' % plist)
        self.run('/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" %s' % plist)
        tmp = self.temp('.ipa')
        self.run('/usr/bin/xcrun -sdk iphoneos PackageApplication -v "%s" -o "%s" --embed "%s"'
                 % (self.app, tmp, self.provisioning))
        if Mode.DEBUG in self.target:
            self.arch(tmp, '%s_DEBUG_%s.ipa' % (self.builder.title, self.version))
        elif Mode.ENTERPRISE in self.target or Mode.RELEASE in self.target:
            ipa = '%s_ENTER_%s.ipa' % (self.builder.title, self.version)
            self.arch(tmp, ipa)
            self.arch(os.path.join(self.basedir, self.dSYM), ipa.replace('.ipa', '.dSYM'))
            if self.plist is not None:
                # plist
                path = self.temp('.plist')
                version = '%s%s' % (self.builder.get('%s.build_version' % self.target), self.builder.version)
                plist = self.plist.replace('${version}', version).replace('${ipa}', ipa)
                with codecs.open(path, mode='w') as fout:
                    fout.writelines(plist)
                self.arch(path, '%s_ENTER_%s.plist' % (self.builder.title, self.version))
        elif Mode.STORE in self.target:
            ipa = '%s_APPSTORE_%s.ipa' % (self.builder.title, self.version)
            self.arch(tmp, ipa)
            self.arch(os.path.join(self.basedir, self.dSYM), ipa.replace('.ipa', '.dSYM'))

    def compile(self):
        out = self.run('/usr/bin/xcodebuild -list')
        target = head(out, start='Targets:', line_num=2, found=True)[1].strip()
        info("target[%s]" % target)
        if self.target is Mode.DEBUG:
            out = self.run('/usr/bin/xcodebuild -target %s -configuration Debug build "CODE_SIGN_IDENTITY=%s"'
                           % (target, self.codeSign))
        elif self.target is Mode.RELEASE:
            out = self.run('/usr/bin/xcodebuild -target %s -configuration Release build "CODE_SIGN_IDENTITY=%s"'
                           % (target, self.codeSign))
        elif self.target is Mode.STORE:
            out = self.run('/usr/bin/xcodebuild -target %s -configuration Release build "CODE_SIGN_IDENTITY=%s"'
                           % (target, self.codeSign))
        else:
            Never()
            return
        self.app = grep(out, '(?<=builtin-validationUtility)\s*(.+\.app)', group=1, limit=1)
        self.dSYM = grep(out, 'GenerateDSYMFile (.+\.dSYM)', group=1, found=self.target != Mode.DEBUG)

    def restore_keychains(self):
        self.run('/usr/bin/security list-keychain -s %s' % ' '.join(self.orig_keychains))
        if self.orig_default_keychain is None or self.orig_default_keychain == "":
            pass
        else:
            # patch: 恢复之前的钥匙串的时候允许出错
            self.run('/usr/bin/security default-keychain -d user -s %s' % self.orig_default_keychain, fail=False)

    def build(self):
        self.cd()
        self.updateID()
        self.update_version()
        self.update_keychains()
        self.run('/usr/bin/open %s' % self.provisioning)
        # todo: 检查 "~/Library/MobileDevice/Provisioning Profiles/" 是否成功导入 失败则清理重复项
        self.run('/usr/bin/xcodebuild -showsdks')
        self.compile()
        self.package()
        self.restore_keychains()


class AndroidBuilder(Base):
    def __init__(self, builder, verbose=True):
        Base.__init__(self, builder=builder, verbose=verbose)

    def ant(self, filename='build.xml', target=None):
        info('[ANT] %s %s' % (self.builder.title, self.builder.version))
        ant_bin = '%s/ant' % env('ANT_ROOT', fail=False) \
            if env('ANT_ROOT', fail=False) \
            else '%s/bin/ant' % env('ANT_HOME')
        if filename.startswith('/'):
            build_file = filename
        else:
            build_file = '%s/%s' % (self._pwd, filename)
        build_file = os.path.abspath(build_file)
        run_cmd('%s/tools/android update project -p "%s"' % (env('ANDROID_SDK_ROOT'), self._pwd))
        if not os.path.exists(build_file):
            Fail("Ant的构建文件[%s]不存在" % build_file)
        if target is None:
            self.run('%s -f "%s"' % (ant_bin, build_file))
        else:
            self.run('%s -f "%s" %s' % (ant_bin, build_file, target))

    @staticmethod
    def update_version(path, human_version, build_version):
        replace_in_file(path, 'android:versionCode\s*=\s*"[^"]*"', 'android:versionCode="%s"' % build_version)
        replace_in_file(path, 'android:versionName\s*=\s*"[^"]*"', 'android:versionName="%s"' % human_version)

    def prepare(self):
        Assert("安卓只支持[Debug|Release]", self.target in [Mode.DEBUG, Mode.RELEASE])
        self.builder.has('proj.android')
        # 环境变量检查
        env('ANT_ROOT')
        env('ANDROID_SDK_ROOT')
        env('NDK_ROOT')
        # 更新版本
        self.cd(self.builder.get('proj.android'))
        self.update_version(self.cur('AndroidManifest.xml'),
                            '%s%s' % (self.builder.get('%s.human_version' % self.target), self.version),
                            self.version
                            )

    def build(self):
        self.cd()
        prepare_dir('%s/debug' % self.output)
        self.cd(self.builder.get('proj.android'))
        self.ant(target='clean')
        targetMap = {Mode.DEBUG: "debug", Mode.RELEASE: "release"}
        if Mode.DEBUG in self.target:
            self.ant(target=targetMap[Mode.DEBUG])
            self.arch(self.cur('bin/*-debug.apk'), '%s_DEV_%s.apk' % (self.builder.title, self.builder.version))
        if Mode.RELEASE in self.target:
            self.ant(target=targetMap[Mode.RELEASE])
            self.arch(self.cur('bin/*-release.apk'), '%s_RELEASE_%s.apk' % (self.builder.title, self.builder.version))


class Cocos2dxAndroidBuilder(AndroidBuilder):
    def __init__(self, builder, verbose=True):
        Base.__init__(self, builder=builder, verbose=verbose)

    def prepare(self):
        AndroidBuilder.prepare(self)
        # 环境变量检查
        env('COCOS2DX_ROOT')
        env('COCOS_CONSOLE_ROOT')

    def build(self):
        self.cd()
        self.run('%s/cocos compile -p android' % env('COCOS_CONSOLE_ROOT'))
        AndroidBuilder.build(self)


@try_run
def start(builder, target, clean_output=True):
    prepare_dir(builder.output, clean=clean_output)
    Assert("没有设置模式", target in Mode.ALL or set(target) == set(Mode.ALL))
    builder.target = target
    info("[%s] PREPARE [%s]" % (builder.title, target))
    builder.prepare()
    info("[%s] BUILD [%s]" % (builder.title, target))
    builder.build()
    info("[%s] SUCC [%s]" % (builder.title, target))


def test():
    # bi = Cocos3BI('3DFISH', '1', '/Users/luozhangming/Documents/git/fish')
    # start(iOSBuilder(bi, verbose=True), Mode.RELEASE)
    # start(AndroidBuilder(bi, verbose=True), Mode.RELEASE)
    pass


if __name__ == '__main__':
    test()
